package com.flurgle.camerakit;

import android.graphics.Rect;
import android.graphics.YuvImage;
import android.hardware.Camera;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.View;
import android.widget.Toast;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import static com.flurgle.camerakit.CameraKit.Constants.FLASH_OFF;
import static com.flurgle.camerakit.CameraKit.Constants.FOCUS_CONTINUOUS;
import static com.flurgle.camerakit.CameraKit.Constants.FOCUS_OFF;
import static com.flurgle.camerakit.CameraKit.Constants.FOCUS_TAP;
import static com.flurgle.camerakit.CameraKit.Constants.METHOD_STANDARD;
import static com.flurgle.camerakit.CameraKit.Constants.METHOD_STILL;

@SuppressWarnings("deprecation")
public class Camera1 extends CameraImpl {

    private static final int FOCUS_AREA_SIZE_DEFAULT = 300;
    private static final int FOCUS_METERING_AREA_WEIGHT_DEFAULT = 1000;

    private int mCameraId;
    private Camera mCamera;
    private Camera.Parameters mCameraParameters;
    private Camera.CameraInfo mCameraInfo;
    private Size mPreviewSize;
    private Size mCaptureSize;
    private Size mVideoSize;
    private MediaRecorder mMediaRecorder;
    private File mVideoFile;
    private Camera.AutoFocusCallback mAutofocusCallback;

    private int mDisplayOrientation;

    @Facing
    private int mFacing;

    @Flash
    private int mFlash;

    @Focus
    private int mFocus;

    @Method
    private int mMethod;

    @Zoom
    private int mZoom;


    Camera1(CameraListener callback, PreviewImpl preview) {
        super(callback, preview);
        preview.setCallback(new PreviewImpl.Callback() {
            @Override
            public void onSurfaceChanged() {
                if (mCamera != null) {
                    setupPreview();
                    adjustCameraParameters();
                }
            }
        });

        mCameraInfo = new Camera.CameraInfo();

    }

    // CameraImpl:

    @Override
    void start() {
        try {
            setFacing(mFacing);
            openCamera();
            if (mPreview.isReady())
                setupPreview();
            mCamera.startPreview();

        } catch (Exception ex) {
            mCameraListener.onCameraError("Aplikasi lain mungkin sedang merekam audio atau video, silahkan coba lagi dalam beberapa saat", ex);
        }

    }

    @Override
    void stop() {
        if (mCamera != null)
            mCamera.stopPreview();
        releaseCamera();
    }

    @Override
    void setDisplayOrientation(int displayOrientation) {
        this.mDisplayOrientation = displayOrientation;
    }

    @Override
    void setFacing(@Facing int facing) {
        int internalFacing = new ConstantMapper.Facing(facing).map();
        if (internalFacing == -1) {
            return;
        }

        for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) {
            Camera.getCameraInfo(i, mCameraInfo);
            if (mCameraInfo.facing == internalFacing) {
                mCameraId = i;
                mFacing = facing;
                break;
            }
        }

        if (mFacing == facing && isCameraOpened()) {
            stop();
            start();
        }
    }

    @Override
    void setFlash(@Flash int flash) {
        if (mCameraParameters != null) {
            List<String> flashes = mCameraParameters.getSupportedFlashModes();
            String internalFlash = new ConstantMapper.Flash(flash).map();
            if (flashes != null && flashes.contains(internalFlash)) {
                mCameraParameters.setFlashMode(internalFlash);
                mFlash = flash;
            } else {
                String currentFlash = new ConstantMapper.Flash(mFlash).map();
                if (flashes == null || !flashes.contains(currentFlash)) {
                    mCameraParameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
                    mFlash = FLASH_OFF;
                }
            }

            mCamera.setParameters(mCameraParameters);
        } else {
            mFlash = flash;
        }
    }

    @Override
    void setFocus(@Focus int focus) {
        this.mFocus = focus;
        switch (focus) {
            case FOCUS_CONTINUOUS:
                if (mCameraParameters != null) {
                    detachFocusTapListener();
                    final List<String> modes = mCameraParameters.getSupportedFocusModes();
                    if (modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
                        mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
                    } else {
                        setFocus(FOCUS_OFF);
                    }
                }
                break;

            case FOCUS_TAP:
                if (mCameraParameters != null) {
                    attachFocusTapListener();
                    final List<String> modes = mCameraParameters.getSupportedFocusModes();
                    if (modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
                        mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
                    }
                }
                break;

            case FOCUS_OFF:
                if (mCameraParameters != null) {
                    detachFocusTapListener();
                    final List<String> modes = mCameraParameters.getSupportedFocusModes();
                    if (modes.contains(Camera.Parameters.FOCUS_MODE_FIXED)) {
                        mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_FIXED);
                    } else if (modes.contains(Camera.Parameters.FOCUS_MODE_INFINITY)) {
                        mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_INFINITY);
                    } else {
                        mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
                    }
                }
                break;
        }
    }

    @Override
    void setMethod(@Method int method) {
        this.mMethod = method;
    }

    @Override
    void setZoom(@Zoom int zoom) {
        this.mZoom = zoom;
    }

    @Override
    void captureImage() {
        try {
            switch (mMethod) {
                case METHOD_STANDARD:
                    mCamera.takePicture(null, null, null, new Camera.PictureCallback() {
                        @Override
                        public void onPictureTaken(byte[] data, Camera camera) {
                            mCameraListener.onPictureTaken(data);
                            camera.startPreview();
                        }
                    });
                    break;

                case METHOD_STILL:
                    mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() {
                        @Override
                        public void onPreviewFrame(byte[] data, Camera camera) {
                            new Thread(new ProcessStillTask(data, camera, mCameraInfo, new ProcessStillTask.OnStillProcessedListener() {
                                @Override
                                public void onStillProcessed(final YuvImage yuv) {
                                    mCameraListener.onPictureTaken(yuv);
                                }
                            })).start();
                        }
                    });
                    break;
            }
        } catch (Exception e) {
            mCameraListener.onCameraError("capture image error:" + e.getMessage(), e);
        }
    }

    int recordTime;

    @Override
    void startVideo() {
        try {
            initMediaRecorder();
            prepareMediaRecorder();
            mMediaRecorder.start();
            mCameraListener.onRecordStart();
        } catch (Exception e) {
            mCameraListener.onCameraError("start failed", e);
        }
    }

    @Override
    void pauseVideo() {
        try {
            mMediaRecorder.stop();
        } catch (RuntimeException e) {
            mCameraListener.onCameraError("stop failed", e);
        }
        mMediaRecorder.release();
        mMediaRecorder = null;
        mCameraListener.onRecordPause();
    }

    @Override
    void endVideo() {
        pauseVideo();

        mVideoFile = new File(mPreview.getView().getContext().getExternalFilesDir(null), "video.mp4");
        if (mVideoFile.exists()) {
            mVideoFile.delete();
        }

        //        ArrayList<String> files = new ArrayList<>();
        //
        //        for (int i = 0; i < recordTime; i++) {
        //            File file = new File(mPreview.getView().getContext().getExternalFilesDir(null), "video" + i + ".mp4");
        //            try {
        //                if (file.exists() && new IsoFile(new FileDataSourceImpl(file)).getMovieBox() != null) {
        //                    files.add(file.getAbsolutePath());
        //                }
        //            } catch (Exception e) {
        //                e.printStackTrace();
        //            }
        //        }
        try {
            //
            //            boolean success = Util.mergeMediaFiles(false, files, mVideoFile.getAbsolutePath());
            //
            //            if (!success) {
            //                mCameraListener.onCameraError("video encode failed ,try record again", null);
            //                return;
            File file = new File(mPreview.getView().getContext().getExternalFilesDir(null), "video0.mp4");
            file.renameTo(mVideoFile);
            //            }
            mCameraListener.onVideoTaken(mVideoFile);
        } catch (Exception e) {
            mCameraListener.onCameraError(e.getMessage(), e);
            return;
        } finally {
            for (int i = 0; i < recordTime; i++) {
                File file = new File(mPreview.getView().getContext().getExternalFilesDir(null), "video" + recordTime + ".mp4");
                if (file.exists()) {
                    file.delete();
                }
            }
            recordTime = 0;
        }
    }

    @Override
    Size getCaptureResolution() {
        if (mCaptureSize == null && mCameraParameters != null) {
            TreeSet<Size> sizes = new TreeSet<>();
            for (Camera.Size size : mCameraParameters.getSupportedPictureSizes()) {
                sizes.add(new Size(size.width, size.height));
            }

            TreeSet<AspectRatio> aspectRatios = findCommonAspectRatios(
                    mCameraParameters.getSupportedPreviewSizes(),
                    mCameraParameters.getSupportedPictureSizes()
            );
            AspectRatio targetRatio = aspectRatios.size() > 0 ? aspectRatios.last() : null;

            Iterator<Size> descendingSizes = sizes.descendingIterator();
            Size size;
            while (descendingSizes.hasNext() && mCaptureSize == null) {
                size = descendingSizes.next();
                if (targetRatio == null || targetRatio.matches(size)) {
                    mCaptureSize = size;
                    break;
                }
            }
        }

        return mCaptureSize;
    }

    Size getVideoResolution() {
        if (mCaptureSize == null && mCameraParameters != null) {
            TreeSet<Size> sizes = new TreeSet<>();
            for (Camera.Size size : mCameraParameters.getSupportedVideoSizes()) {
                sizes.add(new Size(size.width, size.height));
            }

            TreeSet<AspectRatio> aspectRatios = findCommonAspectRatios(
                    mCameraParameters.getSupportedPreviewSizes(),
                    mCameraParameters.getSupportedVideoSizes()
            );
            AspectRatio targetRatio = aspectRatios.size() > 0 ? aspectRatios.last() : null;

            Iterator<Size> descendingSizes = sizes.descendingIterator();
            Size size;
            while (descendingSizes.hasNext() && mCaptureSize == null) {
                size = descendingSizes.next();
                if (targetRatio == null || targetRatio.matches(size)) {
                    if (size.getWidth() > 320 || size.getHeight() > 320) {
                        mVideoSize = size;
                    }
                    //                    break;
                }
            }
        }

        return mCaptureSize;
    }

    @Override
    Size getPreviewResolution() {
        if (mPreviewSize == null && mCameraParameters != null) {
            TreeSet<Size> sizes = new TreeSet<>();
            for (Camera.Size size : mCameraParameters.getSupportedPreviewSizes()) {
                sizes.add(new Size(size.width, size.height));
            }

            TreeSet<AspectRatio> aspectRatios = findCommonAspectRatios(
                    mCameraParameters.getSupportedPreviewSizes(),
                    mCameraParameters.getSupportedPictureSizes()
            );
            AspectRatio targetRatio = aspectRatios.size() > 0 ? aspectRatios.last() : null;

            Iterator<Size> descendingSizes = sizes.descendingIterator();
            Size size;
            while (descendingSizes.hasNext() && mPreviewSize == null) {
                size = descendingSizes.next();
                if (targetRatio == null || targetRatio.matches(size)) {
                    mPreviewSize = size;
                    break;
                }
            }
        }

        return mPreviewSize;
    }

    @Override
    boolean isCameraOpened() {
        return mCamera != null;
    }

    // Internal:

    private void openCamera() {
        try {
            if (mCamera != null) {
                releaseCamera();
                mCamera = null;
            }


            mCamera = Camera.open(mCameraId);
            mCameraParameters = mCamera.getParameters();

            adjustCameraParameters();
            mCamera.setDisplayOrientation(
                    calculateCameraRotation(mDisplayOrientation)
            );

            mCameraListener.onCameraOpened();
        } catch (Exception ex) {
            mCameraListener.onCameraError("Aplikasi lain mungkin sedang merekam audio atau video, silahkan coba lagi dalam beberapa saat", ex);
        }

    }

    private void setupPreview() {
        try {
            if (mPreview.getOutputClass() == SurfaceHolder.class) {
                mCamera.setPreviewDisplay(mPreview.getSurfaceHolder());
            } else {
                mCamera.setPreviewTexture(mPreview.getSurfaceTexture());
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void releaseCamera() {
        if (mCamera != null) {
            mCamera.setPreviewCallback(null);
            mCamera.release();
            mCamera = null;
            mCameraParameters = null;
            mPreviewSize = null;
            mCaptureSize = null;
            mCameraListener.onCameraClosed();
        }
    }

    private int calculateCameraRotation(int rotation) {
        if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            return (360 - (mCameraInfo.orientation + rotation) % 360) % 360;
        } else {
            return (mCameraInfo.orientation - rotation + 360) % 360;
        }
    }

    private synchronized void adjustCameraParameters() {
        //TODO:review other day;
        Size previewResolution = getPreviewResolution();
        if (previewResolution == null) {
            previewResolution = new Size(320, 240);
        }

        if (mCameraParameters == null) {
            mCameraParameters = mCamera.getParameters();
        }

        mPreview.setTruePreviewSize(
                previewResolution.getWidth(),
                previewResolution.getHeight()
        );

        mCameraParameters.setPreviewSize(
                previewResolution.getWidth(),
                previewResolution.getHeight()
        );

        mCameraParameters.setPictureSize(
                getCaptureResolution().getWidth(),
                getCaptureResolution().getHeight()
        );
        int rotation = (calculateCameraRotation(mDisplayOrientation)
                + (mFacing == CameraKit.Constants.FACING_FRONT ? 180 : 0)) % 360;
        mCameraParameters.setRotation(rotation);

        setFocus(mFocus);
        setFlash(mFlash);

        mCamera.setParameters(mCameraParameters);
    }

    private TreeSet<AspectRatio> findCommonAspectRatios(List<Camera.Size> previewSizes, List<Camera.Size> captureSizes) {
        Set<AspectRatio> previewAspectRatios = new HashSet<>();
        for (Camera.Size size : previewSizes) {
            if (size.width >= CameraKit.Internal.screenHeight && size.height >= CameraKit.Internal.screenWidth) {
                previewAspectRatios.add(AspectRatio.of(size.width, size.height));
            }
        }

        Set<AspectRatio> captureAspectRatios = new HashSet<>();
        for (Camera.Size size : captureSizes) {
            captureAspectRatios.add(AspectRatio.of(size.width, size.height));
        }

        TreeSet<AspectRatio> output = new TreeSet<>();
        for (AspectRatio aspectRatio : previewAspectRatios) {
            if (captureAspectRatios.contains(aspectRatio)) {
                output.add(aspectRatio);
            }
        }

        return output;
    }

    //    mMediaRecorder = new MediaRecorder();
    //        mCamera.unlock();
    //
    //        mMediaRecorder.setCamera(mCamera);
    //        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
    //        mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
    //        mMediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_480P));
    //
    //    mVideoFile = new File(mPreview.getView().getContext().getExternalFilesDir(null), "video"+ recordTime+++".mp4");
    //        mMediaRecorder.setOutputFile(mVideoFile.getAbsolutePath());
    private void initMediaRecorder() {
        mMediaRecorder = new MediaRecorder();
        mCamera.unlock();

        mMediaRecorder.setCamera(mCamera);
        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
        mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
        // CamcorderProfile may be null,check it
        CamcorderProfile profile = null;
        if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_480P)) {
            profile = CamcorderProfile.get(CamcorderProfile.QUALITY_480P);
        } else if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_LOW)) {
            profile = CamcorderProfile.get(CamcorderProfile.QUALITY_LOW);
        } else if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_TIME_LAPSE_480P)) {
            profile = CamcorderProfile.get(CamcorderProfile.QUALITY_TIME_LAPSE_480P);
        } else {
            mCameraListener.onCameraError("CamcorderProfile params didn't found", null);
            Log.e("Camera1", "CamcorderProfile params didn't found");
            return;
        }
        //        profile.videoCodec = MediaRecorder.VideoEncoder.H264;
        //        profile.fileFormat = MediaRecorder.OutputFormat.MPEG_4;
        //        profile.audioCodec = MediaRecorder.AudioEncoder.HE_AAC;
        //        mMediaRecorder.setProfile(profile);

        mMediaRecorder.setOutputFormat(profile.fileFormat);
        mMediaRecorder.setVideoFrameRate(profile.videoFrameRate);
        mMediaRecorder.setVideoSize(profile.videoFrameWidth, profile.videoFrameHeight);
        mMediaRecorder.setVideoEncodingBitRate(profile.videoBitRate);
        mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
        mMediaRecorder.setAudioEncodingBitRate(profile.audioBitRate);
        mMediaRecorder.setAudioChannels(profile.audioChannels);
        mMediaRecorder.setAudioSamplingRate(profile.audioSampleRate);
        mMediaRecorder.setAudioEncoder(profile.audioCodec);


        mVideoFile = new File(mPreview.getView().getContext().getExternalFilesDir(null), "video" + recordTime++ + ".mp4");
        mMediaRecorder.setOutputFile(mVideoFile.getAbsolutePath());
        //        mMediaRecorder.setVideoEncodingBitRate(1024*1024*5);
        //        mMediaRecorder.setVideoEncodingBitRate(1024*1024*5);

        //        Size size = getVideoResolution();
        //        mMediaRecorder.setVideoSize(size.getWidth(), size.getHeight());

        //        mMediaRecorder.setMaxDuration(5000);
        //        mMediaRecorder.setMaxFileSize(5000000);
        mMediaRecorder.setOrientationHint(mCameraInfo.orientation);
    }

    private void prepareMediaRecorder() {
        try {
            mMediaRecorder.prepare();
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    void setTapToAutofocusListener(Camera.AutoFocusCallback callback) {
        if (this.mFocus != FOCUS_TAP) {
            throw new IllegalArgumentException("Please set the camera to FOCUS_TAP.");
        }

        this.mAutofocusCallback = callback;
    }

    private int getFocusAreaSize() {
        return FOCUS_AREA_SIZE_DEFAULT;
    }

    private int getFocusMeteringAreaWeight() {
        return FOCUS_METERING_AREA_WEIGHT_DEFAULT;
    }

    private void detachFocusTapListener() {
        mPreview.getView().setOnTouchListener(null);
    }

    private void attachFocusTapListener() {
        mPreview.getView().setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (event.getAction() == MotionEvent.ACTION_UP) {
                    if (mCamera != null) {
                        try {
                            Camera.Parameters parameters = mCamera.getParameters();
                            if (parameters.getMaxNumMeteringAreas() > 0) {
                                Rect rect = calculateFocusArea(event.getX(), event.getY());

                                parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
                                List<Camera.Area> meteringAreas = new ArrayList<>();
                                meteringAreas.add(new Camera.Area(rect, getFocusMeteringAreaWeight()));
                                parameters.setFocusAreas(meteringAreas);
                                parameters.setMeteringAreas(meteringAreas);

                                mCamera.setParameters(parameters);
                                mCamera.autoFocus(new Camera.AutoFocusCallback() {
                                    @Override
                                    public void onAutoFocus(boolean success, Camera camera) {
                                        camera.cancelAutoFocus();
                                        Camera.Parameters params = camera.getParameters();
                                        if (params.getFocusMode() != Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE) {
                                            params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
                                            params.setFocusAreas(null);
                                            params.setMeteringAreas(null);
//                                        camera.setParameters(params);
                                        }

                                        if (mAutofocusCallback != null) {
                                            mAutofocusCallback.onAutoFocus(success, camera);
                                        }
                                    }
                                });
                            } else {
                                mCamera.autoFocus(new Camera.AutoFocusCallback() {
                                    @Override
                                    public void onAutoFocus(boolean success, Camera camera) {
                                        if (mAutofocusCallback != null) {
                                            mAutofocusCallback.onAutoFocus(success, camera);
                                        }
                                    }
                                });
                            }
                        } catch (Throwable t) {
                            Log.e("Camera1", "" + t.getMessage());
                            mCameraListener.onCameraError("" + t.getMessage(),new Exception(t));
                        }
                    }
                }
                return true;

            }
        });
    }

    private Rect calculateFocusArea(float x, float y) {
        int centerX = clamp(Float.valueOf((x / mPreview.getView().getWidth()) * 2000 - 1000).intValue(), getFocusAreaSize());
        int centerY = clamp(Float.valueOf((y / mPreview.getView().getHeight()) * 2000 - 1000).intValue(), getFocusAreaSize());
        return new Rect(
                centerX - getFocusAreaSize() / 2,
                centerY - getFocusAreaSize() / 2,
                centerX + getFocusAreaSize() / 2,
                centerY + getFocusAreaSize() / 2
        );
    }

    private int clamp(int touchCoordinateInCameraReper, int focusAreaSize) {
        int result;
        if (Math.abs(touchCoordinateInCameraReper) + focusAreaSize / 2 > 1000) {
            if (touchCoordinateInCameraReper > 0) {
                result = 1000 - focusAreaSize / 2;
            } else {
                result = -1000 + focusAreaSize / 2;
            }
        } else {
            result = touchCoordinateInCameraReper - focusAreaSize / 2;
        }
        return result;
    }


}